﻿using System;
using System.Collections.Generic;
using System.Linq;
using CommonUtils;

namespace SearchFrontEnd
{
    using BackEndServerList = ServerList<SearchBackendClient, ISearchBackend>;

    /// <summary>
    /// Implementation of the interface between the front end server and the
    /// clients.
    /// </summary>
    public class SearchFrontEnd : ISearchFrontEnd
    {
        private static Cache searchResultsCache = new Cache(FrontEndServer.CACHE_SIZE);

        /// <summary>
        /// Used by a client to index a certain page in the search engine.
        /// </summary>
        /// <param name="page">The page to be indexed.</param>
        public void Index(Page page)
        {
            Print.DebugEmit("IndexPage: " + page.Name);

            // Forward the index request to some BE server, chosen in RR order.
            if (BackEndServerList.IsEmpty())
            {
                // no registered servers, can't perform the operation.
                Print.DebugEmit("No BE servers available, returning..");
                return;
            }

            // create the index page
            SearchBackEnd.Page pageToIndex = new SearchBackEnd.Page
            {
                Name = page.Name,
                Content = page.Content
            };

            // forward the request
            bool done = false;
            SearchBackendClient client = BackEndServerList.GetNext();
            while (!done)
            {
                Print.DebugEmit(
                    String.Format(
                               "Trying to forwarded index request to BE {0}...",
                               client.Endpoint.Address
                               )
                    );

                try
                {
                    client.Index(pageToIndex);
                }
                catch
                {
                    // if an exception occurs when accessing the BE server
                    // we assume that it is down and remove it from the list.
                    // Then try approaching the next BE server.
                    SearchBackendClient tmp = client;
                    client = BackEndServerList.GetNext();
                    BackEndServerList.Remove(tmp);
                    FEGroupCommunication.NotifyBackEndServerIsDown(
                         client.Endpoint.Address
                         );
                    if (client.Endpoint.Equals(tmp.Endpoint))
                    {
                        // no more BE servers available
                        Print.DebugEmit("No BE servers available, returning..");
                        return;
                    }
                    continue;
                }
                Print.DebugEmit("Done.");
                InvalidateStaleCacheEntries(page.Content);
                FEGroupCommunication.NotifyCacheInvalidateNeeded(page.Content);
                done = true;
            }
        }

        /// <summary>
        /// Used by a client to search for pages on the search engine that match
        /// the search criteria.
        /// </summary>
        /// <param name="words">list of words to search for.</param>
        /// <returns>
        /// A list of pages, where each page contains all the specified words.
        /// </returns>
        public string[] Search(String[] words)
        {
            String[] results = null;

            // Sort search words
            Array.Sort(words);
            string key = Print.ArrayAsString(words);

            Print.DebugEmit(String.Format("Search: [{0}]" ,Print.ArrayAsString(words)));
            // Forward the search request to all BE servers and aggregate 
            // results
            HashSet<String> searchResults = new HashSet<string>();


            // Check whether the item is cached
            results = searchResultsCache.GetValue(key);
            if (results != null)
            {
                // Return the cached search results
                Print.DebugEmit("Found results in cache.");
                return results;
            }

            Print.DebugEmit("Results were not cached.");
            
            // The search results were not cached - run the search.
            int numValidResults = 0;
            while (numValidResults != BackEndServerList.Count)
            {
                // iterate the servers' list and send the search request to each one
                foreach (SearchBackendClient client in BackEndServerList.GetCopy())
                {
                    try
                    {
                        string[] currResults = client.Search(words);
                        ConcatResults(searchResults, currResults);
                        numValidResults++;
                    }
                    catch (Exception e)
                    {
                        Print.DebugEmit("error while searching " + e.Message);
                        // if an exception occurs when accessing the back end server
                        // we assume that it is down and remove it from the list.
                        // In this case we have to restart the search operation to
                        // make sure we get correct results.
                        BackEndServerList.Remove(client);
                        FEGroupCommunication.NotifyBackEndServerIsDown(client.Endpoint.Address);
                        searchResults.Clear();
                        numValidResults = 0;
                        break;
                    }
                }
            }

            results = searchResults.ToArray();

            // Sort search results in alphabetical order
            Array.Sort(results);

            // Cache the results
            searchResultsCache.Add(key, results);

            return results;
        }

        /// <summary>
        /// Add all of the current results into the search results set.
        /// </summary>
        /// <param name="searchResults">Search results computed so far</param>
        /// <param name="currResults">Search results received from the
        /// current BE server.</param>
        private static void ConcatResults(
                                    HashSet<String> searchResults,
                                    IEnumerable<string> currResults
                                    )
        {
            foreach (string res in currResults)
            {
                searchResults.Add(res);
            }
        }

        /// <summary>
        /// Removes all the entries in the results cache that become stale
        /// as a result of a recent index operation.
        /// </summary>
        /// <param name="indexedContent">The content of the indexed page</param>
        public static void InvalidateStaleCacheEntries(string indexedContent)
        {
            if (searchResultsCache.Count == 0)
            {
                // Nothing to invalidate
                return;
            }

            Cache tmpCache = new Cache(FrontEndServer.CACHE_SIZE);
            char[] delim = {' '};
            HashSet<string> words = new HashSet<string>(indexedContent.Split(delim));
            foreach (string key in searchResultsCache.GetKeys())
            {
                string[] innerWords = key.Split(delim);

                // if the keywords in the current key do not contain any words
                // that appear in the indexed document, we can keep the search
                // results.
                if (words.Intersect(innerWords).Count() == 0)
                {
                    tmpCache.Add(key, searchResultsCache.GetValue(key));
                }
            }

            // switch caches
            searchResultsCache = tmpCache;
        }
    }
}
